package clw.toy;

import clw.Toy;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author chengliwei
 * @date 2023/11/17 18:04
 */
public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {

    final Environment globals = new Environment();
    private Environment environment = globals;

    private final Map<Expr, Integer> locals = new HashMap<>();

    public Interpreter() {
        globals.define("millis", new Functionable() {
            @Override
            public int arity() {
                return 0;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                return System.currentTimeMillis();
            }
        });
        globals.define("type", new Functionable() {
            @Override
            public int arity() {
                return 1;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                return arguments.get(0).getClass().getName();
            }
        });
        globals.define("List", new NativeClass(ArrayList.class));
        globals.define("Map", new NativeClass(HashMap.class));
    }

    public void interpret(List<Stmt> statements) {
        try {
            for (Stmt statement : statements) {
                execute(statement);
            }
        } catch (RuntimeError error) {
            Toy.runtimeError(error);
        }
    }

    private void execute(Stmt stmt) {
        stmt.accept(this);
    }

    private String stringify(Object object) {
        if (object == null) {
            return "nil";
        }
        if (object instanceof Double) {
            String text = object.toString();
            if (text.endsWith(".0")) {
                text = text.substring(0, text.length() - 2);
            }
            return text;
        }
        return object.toString();
    }

    @Override
    public Object visitBinary(Expr.Binary binary) {
        Object left = evaluate(binary.left);
        Object right = evaluate(binary.right);

        switch (binary.operator.type) {
            case MINUS:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left - (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left - (Integer) right;
                }
                return (double) left - (double) right;
            case SLASH:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left / (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left / (Integer) right;
                }
                return (double) left / (double) right;
            case STAR:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left * (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left * (Integer) right;
                }
                return (double) left * (double) right;
            case PLUS:
                if (left instanceof Double && right instanceof Double) {
                    return (double) left + (double) right;
                }
                if (left instanceof String || right instanceof String) {
                    return left.toString() + right.toString();
                }
                if (left instanceof Long) {
                    return (Long) left + (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left + (Integer) right;
                }
                break;
            case GREATER:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left > (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left > Integer.valueOf(right.toString());
                }
                return (double) left > (double) right;
            case GREATER_EQUAL:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left >= (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left >= Integer.valueOf(right.toString());
                }
                return (double) left >= (double) right;
            case LESS:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left < Long.valueOf(right.toString());
                }
                if (left instanceof Integer) {
                    return (Integer) left < Integer.valueOf(right.toString());
                }
                return (double) left < (double) right;
            case LESS_EQUAL:
                checkNumberOperand(binary.operator, left, right);
                if (left instanceof Long) {
                    return (Long) left <= (Long) right;
                }
                if (left instanceof Integer) {
                    return (Integer) left <= Integer.valueOf(right.toString());
                }
                return (double) left <= (double) right;
            case BANG_EQUAL:
                return !isEqual(left, right);
            case EQUAL_EQUAL:
                return isEqual(left, right);
            default:
                break;
        }
        return null;
    }

    private boolean isEqual(Object a, Object b) {
        if (a == null && b == null) {
            return true;
        }
        if (a == null) {
            return false;
        }
        return a.equals(b);
    }

    @Override
    public Object visitGrouping(Expr.Grouping grouping) {
        return evaluate(grouping.expression);
    }

    @Override
    public Object visitLiteral(Expr.Literal literal) {
        return literal.value;
    }

    @Override
    public Object visitUnary(Expr.Unary unary) {
        Object right = evaluate(unary.right);
        switch (unary.operator.type) {
            case BANG:
                return isTruthy(right);
            case MINUS:
                checkNumberOperand(unary.operator, right);
                return -(double) right;
            default:
                return null;
        }
    }

    @Override
    public Object visitVariableExpr(Expr.Variable variable) {
        return lookUpVariable(variable.name, variable);
    }

    private Object lookUpVariable(Token name, Expr expr) {
        Integer distance = locals.get(expr);
        if (distance != null) {
            return environment.getAt(distance, name.lexeme);
        } else {
            return globals.get(name);
        }
    }

    @Override
    public Object visitAssignExpr(Expr.Assign assign) {
        Object value = evaluate(assign.value);

        Integer distance = locals.get(assign);
        if (distance != null) {
            environment.assignAt(distance, assign.name, value);
        } else {
            globals.assign(assign.name, value);
        }
        return value;
    }

    @Override
    public Object visitThisExpr(Expr.This thisExpr) {
        return lookUpVariable(thisExpr.keyword, thisExpr);
    }

    @Override
    public Object visitLogicalExpr(Expr.Logical logical) {
        Object left = evaluate(logical.left);
        if (logical.operator.type == TokenType.OR) {
            if (isTruthy(left)) return left;
        } else {
            if (!isTruthy(left)) return left;
        }
        return evaluate(logical.right);
    }

    @Override
    public Object visitCallExpr(Expr.Call call) {
        Object callee = evaluate(call.callee);

        List<Object> arguments = new ArrayList<>();
        for (Expr argument : call.arguments) {
            arguments.add(evaluate(argument));
        }

        if (!(callee instanceof Functionable function)) {
            throw new RuntimeError(call.paren, "Can only call functions and classes.");
        }

        if (function instanceof NativeFunction) {
            return function.call(this, arguments);
        }

        if (arguments.size() != function.arity()) {
            throw new RuntimeError(call.paren, String.format("Expected %s arguments but got %s.", function.arity(), arguments.size()));
        }
        return function.call(this, arguments);
    }

    @Override
    public Object visitGetExpr(Expr.Get get) {
        Object object = evaluate(get.object);
        if (object instanceof ToyInstance) {
            return ((ToyInstance) object).get(get.name);
        }
        if (object instanceof NativeInstance) {
            return ((NativeInstance) object).get(get.name.lexeme);
        }
        throw new RuntimeError(get.name, "Only instances have properties.");
    }

    @Override
    public Object visitSetExpr(Expr.Set set) {
        Object object = evaluate(set.object);
        if (!(object instanceof ToyInstance)) {
            throw new RuntimeError(set.name, "Only instances have fields.");
        }
        Object value = evaluate(set.value);
        ((ToyInstance) object).set(set.name, value);
        return value;
    }

    @Override
    public Object visitSuperExpr(Expr.Super superExpr) {
        int distance = locals.get(superExpr);
        ToyClass superClass = (ToyClass) environment.getAt(distance, "super");
        ToyInstance object = (ToyInstance) environment.getAt(distance - 1, "this");
        Function method = superClass.findMethod(superExpr.method.lexeme);

        if (method == null) {
            throw new RuntimeError(superExpr.method, "Undefined property '" + superExpr.method.lexeme + "'.");
        }
        return method.bind(object);
    }

    @Override
    public Object visitSelfIncrementOrDecrement(Expr.SelfIncrementOrDecrement expr) {
        Expr left = expr.left;
        Token token = ((Expr.Variable) left).name;
        Integer distance = locals.get(left);
        Integer evaluate = (Integer) evaluate(left);
        if (expr.operator.type == TokenType.INC) {
            evaluate++;
        } else if (expr.operator.type == TokenType.DEC) {
            evaluate--;
        }
        if (distance != null) {
            environment.assignAt(distance, token, evaluate);
        } else {
            globals.assign(token, evaluate);
        }
        return null;
    }

    private void checkNumberOperand(Token operator, Object operand) {
        if (operand instanceof Double) {
            return;
        }
        throw new RuntimeError(operator, "Operand must be a number.");
    }

    private void checkNumberOperand(Token operator, Object left, Object right) {
        if (left instanceof Number && right instanceof Number) {
            return;
        }
        throw new RuntimeError(operator, "Operands must be numbers.");
    }

    private boolean isTruthy(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj instanceof Boolean) {
            return (boolean) obj;
        }
        return true;
    }

    private Object evaluate(Expr expr) {
        return expr.accept(this);
    }

    @Override
    public Void visitExpressionStmt(Stmt.Expression expression) {
        evaluate(expression.expr);
        return null;
    }

    @Override
    public Void visitPrintStmt(Stmt.Print print) {
        Object value = evaluate(print.value);
        System.out.println(stringify(value));
        return null;
    }

    @Override
    public Void visitVarStmt(Stmt.Var var) {
        Object value = null;
        if (var.initializer != null) {
            value = evaluate(var.initializer);
        }
        environment.define(var.name.lexeme, value);
        return null;
    }

    @Override
    public Void visitBlockStmt(Stmt.Block block) {
        executeBlock(block.statements, new Environment(environment));
        return null;
    }

    void executeBlock(List<Stmt> statements, Environment environment) {
        Environment previous = this.environment;
        try {
            this.environment = environment;
            for (Stmt statement : statements) {
                execute(statement);
            }
        } finally {
            this.environment = previous;
        }
    }

    @Override
    public Void visitClassStmt(Stmt.Class clazz) {
        Object superClass = null;
        if (clazz.superClass != null) {
            superClass = evaluate(clazz.superClass);
            if (!(superClass instanceof ToyClass)) {
                throw new RuntimeError(clazz.superClass.name, "Super class must be a class.");
            }
        }

        environment.define(clazz.name.lexeme, null);

        Map<String, Function> methods = new HashMap<>();
        for (Stmt.Function method : clazz.methods) {
            Function function = new Function(method, environment, method.name.lexeme.equals("init"));
            methods.put(method.name.lexeme, function);
        }

        ToyClass klass = new ToyClass(clazz.name.lexeme, (ToyClass) superClass, methods);

        if (superClass != null) {
            environment = environment.enclosing;
        }

        environment.assign(clazz.name, klass);
        return null;
    }

    @Override
    public Void visitFunctionStmt(Stmt.Function function) {
        Function fun = new Function(function, environment, false);
        environment.define(function.name.lexeme, fun);
        return null;
    }

    @Override
    public Void visitIfStmt(Stmt.If ifStmt) {
        if (isTruthy(evaluate(ifStmt.condition))) {
            execute(ifStmt.thenBranch);
        } else if (ifStmt.elseBranch != null) {
            execute(ifStmt.elseBranch);
        }
        return null;
    }

    @Override
    public Void visitReturnStmt(Stmt.Return returnStmt) {
        Object value = null;
        if (returnStmt.value != null) {
            value = evaluate(returnStmt.value);
        }
        throw new Return(value);
    }

    @Override
    public Void visitWhileStmt(Stmt.While whileStmt) {
        while (isTruthy(evaluate(whileStmt.condition))) {
            execute(whileStmt.body);
        }
        return null;
    }

    public void resolve(Expr expr, int depth) {
        locals.put(expr, depth);
    }
}
